从布局和实现的角度,聊聊 Notification
作者简介
本文由 强波 原创并授权发布,未经原作者允许请勿转载。
本文从实现的角度,讲解了 Android 的 Notification ,写的非常的细致,推荐大家看一下 ,希望大家喜欢。
强波 的博客地址:
http://qiangbo.space/2017-07-07/AndroidAnatomy_Notification/
在本文中,我们来详细了解一下Android 上的 Notification 实现。
Notification 是自 Android 发布以来就有的 API ,也是应用程序中最常用的功能的之一,开发者对其应当是相当的熟悉了。
在 Android 近几年的版本更新中,几乎每个版本都会对系统通知界面,以及相关API做一些的改变。这些改变使得开发者可以更好的控制应用程序的通知样式,同时也使得通知功能更易于用户使用。
本文我们来详细看一下 Notification 方面的知识。
开发者API
这里不打算对 Notification 基本的使用方式做过多讲解,这方面内容对于很多开发者来说都已经是非常熟悉的了,并且网络上也很容易搜索到相关内容。
下面只会说明 Notification 自 Android 5.0 以来的新增加功能。
Heads-up Notification
Heads-up Notification 是 Android 5.0 上的新增功能。
当设备处于使用状态下(已经解锁并且屏幕亮着)时,这种通知以一个小的浮动窗口的形式呈现出来,就像下面这样:
这个样式看起来像是对通知的一种压缩,但是 Heads-up Notification 可以包含 Action Button 。用户可以点击 Action Button 进行相应的操作,也可以将这个通知界面移除掉但是不离开当前应用。
这对于用户体验来说是一项非常好的改进,系统的来电通知就是这种形式的通知。在设备处于使用状态下时,这种通知既不会干扰用户当前的行为(可以直接将通知界面移除掉),又方便了用户对于通知的处理(可以直接点击 Action Button 来处理通知)。
只要 Notification 满足下面的两种情况下任何一种,就会产生 Heads-up Notification:
Notification 设置了 fullScreenIntent。
Notification 是一个 High 优先级的通知并且使用了铃声或震动。
锁屏上的Notification
从 Android 5.0 开始,通知可以在锁屏上显示。开发者可以利用这个特性来实现媒体播放按钮或者其他常用的操作。但同时,用户也可以通过设置来决定是否在锁屏界面上显示某个应用的通知。
开发者可以通过Notification.Builder.setVisibility(int)
方法来控制通知显示的详细级别。这个方法接收三个级别的控制:
VISIBILITY_PUBLIC 显示通知的全部内容。
VISIBILITY_PRIVATE 显示通知的基本信息,例如通知的 icon 和 title ,但是不显示详细内容。
VISIBILITY_SECRET 不显示通知的任何内容。
Notification直接回复
从 Android 7.0 开始,用户可以在通知界面上进行直接回复。直接回复按钮附加在通知的下面。
当用户通过键盘回复时,系统将用户输入的文字附在开发者指定的 Intent 上,然后发送给对应的应用。
创建一个包含直接回复按钮的通知分为下面几个步骤:
创建一个 PendingIntent ,这个 PendingIntent 将在用户输入完成点击发送按钮之后触发。因此我们需要为这个 PendingIntent 设置一个接受者,我们可以使用一个 BroadcastReceiver 来进行接收。
创建一个 RemoteInput.Builder 对象实例,这个类的构造函数接收一个字符串作为 Key 来让系统放入用户输入的文字。在接收方通过这个 key 来获取输入。
通过
Notification.Action.Builder.addRemoteInput()
方法将第1步创建的 RemoteInput 对象添加到Notification.Action 上。创建一个通知包含前面创建的 Notification.Action ,然后发送。
相关代码示例如下:
当用户点击回复按钮时,系统会提示用户输入:
当用户输入完成并点击发送按钮之后,我们设置的replyPendingIntent
被会触发。前面我们设置了一个 BroadcastReceiver 来处理这个 Intent ,于是在 BroadcastReceiver 中可以通过下面这样的方式来获取用户输入的文本:
这里还有两点需要开发者注意的:
用户点击完发送按钮之后,该按钮会变成一个旋转的样式表示这个动作还在进行中。开发者需要重新发送一条新的通知来更新这个状态。
通过 BroadcastReceiver 来处理这个发送事件的同时,请注意将 BroadcastReceiver 在 AndroidManifest.xml 中的配置设为:android:exported=”false” 。否则任何应用都可以发送一条 Intent 来触发你的 BroadcastReceiver ,这可能对你应用造成危害。
Bundling Notifications
从 Android 7.0 开始,系统提供一个新的方式来展示连续的通知: Bundling notifications。
这种展示方式特别适用于即时通讯类应用,因为这类应用会持续不断的收到新的消息并发送通知。这种展示方式是以一种层次性的结构来组织通知。顶部是显示组内概览信息的消息,当用户进一步展开组的时候,系统显示组内的更多信息。如下图所示:
Notification.Build 类中提供了相应的API来进行这种通知样式的管理:
Notification.Builder.setGroup(String groupKey)
通过 groupKey 将通知归为一个组Notification.Builder.setGroupSummary(boolean isGroupSummary)
当 isGroupSummary = true 时表示将该条通知设为组内的 Summary 通知Notification.Builder.setSortKey(String sortKey)
系统将根据这里设置的 sortKey 进行排序
Notification 消息样式
从 Android 7.0 开始,系统提供了 MessagingStyle API 来自定义通知的样式。开发者可以自定义通知的各种 Label,包括:对话 Title ,附加消息以及通知的 Content view 等。下面是一段代码示例:
这条通知显示出来是下面这个样子:
通知栏与通知窗口
外部界面
通知栏位于状态栏中,在状态栏的左侧通过一系列应用的 Icon 来显示通知:
用户可以通过从屏幕上侧下滑的方法展开通知窗口,通知窗口的上方是 Quick Settings 区域,下方是通知列表。用户可以展开 Quick Settings 区域。
内部实现
在了解了通知界面的外观之后,我们就来看一下系统是如何实现这个界面的。
在 SystemUI 的实现中,通过 XML 布局文件以及一系列自定义 Layout 类来管理通知界面。
整个 Status Bar 通过 super_status_bar.xml 文件来进行布局,这个布局文件的根元素是一个自定义的 FrameLayout ,类名是 StatusBarWindowView 。这个布局文件的结构如下图所示:
在这里,我们重点要关注的就是选中的两行:
super_status_bar.xml 中 include 了一个名称为 status_bar 的布局文件。
super_status_bar.xml 中 include 了一个名称为 status_bar_expanded 的布局文件。
这里的 status_bar 便是系统状态栏的布局文件, status_bar_expanded 便是下拉的通知窗口的布局文件。
status_bar.xml 布局文件结构如下图所示。这个布局文件的根元素是名称为 PhoneStatusBarView 的自定义 FrameLayout 类。
对照这个布局文件和手机上的状态栏,我相信读者应该很容易理解了:
notification_icon_area 正是系统显示通知 icon 的区域
system_icon_area 是显示系统图标的区域,例如:Wifi,电话信息以及电池等
clock 是状态栏上显示时间的区域
下面我们再来看一下 status_bar_expanded.xml 这个布局文件的结构,这个布局文件的根元素是一个名称为 NotificationPanelView 的类,这个类同样是一个自定义的 FrameLayout 。
在这个布局文件中:
顶部是一个名称为 keyguard_status_view 的元素。这个便是该界面上的状态栏布局。这个状态栏显示的内容和通常的状态栏的内容是有所区别的,读者可以回到上面相应的截图对比一下不同场景下状态栏显示的内容。
qs_auto_reinflate_container 是显示 Quick Settings 的区域。这个区域其实是 include 了一个另外布局文件:qs_panel.xml。
notification_stack_scroller 便是真正显示通知列表的地方,这是一个 NotificationStackScrollLayout 类型的元素。从名称上我们就可以看出,这个元素是可以滚动的,因为通知的列表可能是很长的。
上面我只大概讲解了这些界面中最主要的元素,而实际上布局中还有非常多的其他元素。这里我们就不一一讲解了。读者可以借助 Android Studio 上的 Layout Inspector 工具选择com.android.systemui 进程,然后选择 StatusBar 来详细分析该界面上的每一个元素,Layout Inspector 界面看起来像下面这样:
Notification从发送到显示
Notification的发送
有了上面通知界面布局的知识之后,我们再看一下,应用程序中发送的通知是如何最终显示到系统的通知界面上的。
开发者通过创建 Notification 对象来发送通知。该对象中记录了一条通知的所有详细信息,Notification 类图如下所示:
这里的很多字段相信开发者都很熟悉,因为这些字段都是我们发送通知时要设置的。这里需要说明的是Bundle extras
这个字段。Bundle 以键值对的形式存储了可以通过 IPC 传递的一系列数据。当我们通过 Notification.buidler 构建 Notification 对象时,有一些自定义样式的值都是存在这个 extras 字段中的,例如下面这些:
Notification 类是一个 Parcelable 类,这意味着它可以通过Binder 被跨进程传递。
我们通常不会手动创建 Notification ,而是通过Notification.Builder 类中的 setXXX 方法(上面已经列出了一些)来创建 Notification。很显然,这个 Notification.Builde r类使用的是典型的 Builder 设计模式,通过这个类,简化了我们创建 Notification 的过程,下图是 Notification.Builde r类的类图:
这个类提供了非常多的 setXXX 方法让我们设置 Notification 的属性,并且这些方法会返回 Builder 对象本身以便我们可以连续调用。最终,我们通过一个build
方法获取到构造好的 Notification 对象。
NotificationManagerService
在构造好了 Notification 对象之后,我们通过NotificationManager 的public void notify(int id, Notification notification)
(及其重载)方法真正将通知发送出去。
我相信读者自然能想到,这个 NotificationManager 一定也是通过 Binder 实现的。
确实没错,真正实现通知发送的服务叫做 NotificationManagerService ,这个 Service 同样位于system_server 进程中。
NotificationManager 代表了服务的客户端被应用程序所使用,而 NotificationManagerService 位于系统进程中接收和处理请求。 Android 系统中大量的系统服务都是这样的实现套路。
notify 接口最终会调用到 NotificationManager 中的另一个叫做 notifyAsUser 的接口来发送通知,其实现如下:
这段代码说明如下:
通过 getService 方法获取 NotificationManagerService 的远程服务接口, getService 方法的实现其实就是通过ServiceManager 拿到 NotificationManagerService 的 Binder 对象。
通过 mContext 为 Notification 添加一些附加属性,这里的 mContext 代表了调用发送通知接口的 Context ,系统服务中会通过这个 Context 来确定是谁在使用服务。
在 LOLLIPOP_MR1 之上的版本(API Level 22)上,发送通知必须设置 Small Icon ,否则直接抛出异常。
调用 NotificationManagerService 的远程接口来真正进行通知的发送。
接下来我们要关注的自然是NotificationManagerService.enqueueNotificationWithTag方法的实现。
NotificationManagerService相关代码位于以下路径:/frameworks/base/services/core/java/com/android/server/notification/
在NotificationManagerService.enqueueNotificationWithTag方法中,会将用户发送过来的 Notification 对象包装在一个 StatusBarNotification 对象中:
然后又将 StatusBarNotification 包装在 NotificationRecord 对象中:
StatusBarNotification 构造函数中的其他参数,描述了发送通知的调用者的身份,包括:包名,调用者的 uid,pid 等等。这个身份的作用是:系统可以针对调用者身份的不同做不同的处理。例如:用户可能关闭了某些应用的通知显示,系统通过调用者的身份便可以确定这个应用的通知是否需要显示在通知界面上。
而看到 NotificationRecord,读者应该很自然能想到ActivityManagerService 中的 ActivityRecord,ProcessRecord 等结构。这些都是系统服务中用来描述应用程序中对象的对应结构。
下图描述了上面三种结构的包含关系:
系统在创建 NotificationRecord 对象之后,会 Post一个Runnable 的 Task 进行通知的发送:
在 EnqueueNotificationRunnable 中,需要做下面几件事情:
处理通知的分组。
检查该通知是否已经被阻止(通过调用者的身份:包名及 uid )。
对通知进行排序。
判断对已有通知更新,还是发送一条新的通知。
调用 NotificationListeners.notifyPostedLocked。
如果需要:处理声音和震动。
这里只有NotificationListeners.notifyPostedLocked
需要说明一下。
一条通知发送到系统之后,系统中可能会有很多模块会对其感兴趣(最基本的,会有模块要将这个通知显示在通知界面上)。发送通知是一个事件,处理通知是一个响应,当事件的响应者可能不止一个的时候,为了达到解耦这两者之间的关系,很自然的会使用我们常见的监听器模型(或者叫做:Observer 设计模式)。
系统中,对于通知感兴趣的监听器通过 NotificationListenerService 类来表达。而这里的NotificationListeners.notifyPostedLocked
便是对所有的 NotificationListenerService 进行回调通知。
这其中有一个最重要的 NotificationListenerService 就是 BaseStatusBar 。因为它就是负责将通知显示在通知界面上的监听器。
Notification的显示
BaseStatusBar 中对于通知发送的回调逻辑如下:
这段代码的说明如下:
每个 StatusBarNotification 对象都有一个 Key 值,这个值根据调用者的身份以及调用者设置的通知 id 生成。当应用程序通过同一个通知 id 发送了多次通知,这些通知的 Key 值是一样的,由此可以对通知进行更新。
mNotificationData(类型为 NotificationDat a)中记录了系统所有的通知列表。
如果是一个已经存在的通知需要更新,则先将存在的通知删除。
addNotification是一个抽象方法,由子类实现。
在手机设备上,addNotification()
这个方法自然是由PhoneStatusBar 来实现。在 addNotification()
方法中,会调用 updateNotifications()
方法来最终将通知显示在通知界面上,其代码如下所示:
这里的updateNotificationShade方法便是将通知的显示内容添加到通知面板的显示区域:NotificationStackScrollLayout中。而mIconController.updateNotificationIcons(mNotificationData)则是在notification_icon_area区域添加通知Icon。
updateNotificationShade代码比较长,但是逻辑是比较好理解的。主体逻辑就是对每一个需要显示的通知创建一个ExpandableNotificationRow,然后设置对应的内容并添加到NotificationStackScrollLayout(mStackScroller 对象)中。
浏览一下这段代码便可以看到我们在 API 部分讲解的一些 API 在系统服务中的实现:这里了处理通知的分组,visibility 等相关信息。
至此,一条新发送的通知就真正显示出来了。
下面这幅图描述了一条 Notification 从发送到显示出来的流程:
推荐阅读:
点赞或者分享吧~